package rad.framework.seam;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;

import rad.framework.jpa.EntityPersistenceContextMap;
import rad.framework.jpa.JpaUtil;
import rad.framework.lang.ReflectionUtil;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.seam.Component;
import org.jboss.seam.international.StatusMessages;
import org.jboss.seam.international.StatusMessage.Severity;

/**
 * This import utility assumes that your entities are using @NaturalId annotations.
 * 
 * @author boudyacho
 *
 */
public class ImportUtil {

	private static final Log log = LogFactory.getLog(ImportUtil.class);

	public static List<Object> doImport(List<Object> list) {
		Map<String, Object> hits = new HashMap<String, Object>();
		return doImport(list, hits);
	}

	public static List<Object> doImport(List<Object> list,
			Map<String, Object> hits) {
		List<Object> results = new ArrayList<Object>();
		for (Object entity : list) {
			results.add(doImport(entity, hits));
		}
		return results;
	}

	// ---------------------------------------------------------------

	public static Object doImport(Object entity) {
		Map<String, Object> hits = new HashMap<String, Object>();
		return doImport(entity, hits);
	}

	public static Object doImport(Object entity, Map<String, Object> hits) {
		EntityManager em = getEntityManager(entity);
		Object existing = JpaUtil.findForNaturalId((Serializable) entity, em);
		if (existing != null) {
			StatusMessages.instance().add(Severity.INFO,
					"#0 already exists. Not imported.", getNaturalId(entity));
			return existing;
		}

		return doImport(null, entity, em, hits);
	}

	// ---------------------------------------------------------------

	protected static Object doImport(Object entity, EntityManager em,
			Map<String, Object> hits) {
		entity = doImport(null, entity, em, hits);
		return entity;
	}

	protected static Object doImport(Object parent, Object entity,
			EntityManager em, Map<String, Object> hits) {
		Object nid = null;
		if (log.isDebugEnabled()) {
			nid = getNaturalId(entity);
			log.debug("Importing: " + nid);
		}

		// store the collections so that we can merge top-down
		Map<Method, Collection> o2m = collect(entity, OneToMany.class);
		Map<Method, Collection> m2m = collect(entity, ManyToMany.class);

		// these are likely required to merge
		m2o(parent, entity, em, hits);
		o2o(entity, em, hits);

		// now merge
		if (JpaUtil.isIdentityGenerated(entity.getClass())) {
			JpaUtil.setIdentity((Serializable) entity, null);
		}
		entity = em.merge(entity);
		StatusMessages.instance().add(Severity.INFO, "Imported: #0", nid);
		addHit(hits, entity);

		// now do the collections
		m2m(entity, m2m, em, hits);
		o2m(entity, o2m, em, hits);

		return em.merge(entity);
	}

	//---------------------------------------------------------------

	protected static void m2o(Object parent, Object entity, EntityManager em, Map<String, Object> hits) {
		List<Method> associations = ReflectionUtil.findMethodsByAnnotation(
				entity.getClass(), ManyToOne.class);
		for (Method method : associations) {
			Serializable ref = (Serializable) ReflectionUtil.invoke(method,
					entity, new Object[] {});
			if (ref != null) {
				Object ref2 = null;
				if (parent != null) {
					// check that this isn't a ref back to my o2m parent
					String pnid = getNaturalId(parent);
					String rnid = getNaturalId(ref);
					if (pnid.equals(rnid)) {
						ref2 = parent;
					}
				}
				if (ref2 == null) {
					// not parent so see of ref already exists
					ref2 = find(ref, em, hits);
				}
				if (ref2 == null) {
					// does not exist yet
					ref2 = doImport(ref, em, hits);
				}
				set(entity, method.getName(), ref2);
			}
		}
	}
	
	protected static void o2o(Object parent, EntityManager em, Map<String, Object> hits) {
		List<Method> associations = ReflectionUtil.findMethodsByAnnotation(
				parent.getClass(), OneToOne.class);
		for (Method method : associations) {
			Serializable ref = (Serializable) ReflectionUtil.invoke(method,
					parent, new Object[] {});
			if (ref != null) {
				ref = (Serializable) doImport(parent, ref, em, hits);
				set(parent, method.getName(), ref);
			}
		}
	}

	protected static void m2m(Object entity, Map<Method, Collection> data, EntityManager em, Map<String, Object> hits) {
		Set<Method> associations = data.keySet();
		for (Method method : associations) {
			Collection<Serializable> list = data.get(method);
			for (Serializable ref : list) {
				Object ref2 = find(ref, em, hits);
				if (ref2 == null) {
					//System.out.println("Missed M2M Ref: " + getNaturalId(ref) + " for " + getNaturalId(entity));
					ref2 = doImport(ref, em, hits);
				}
				add(method, entity, ref2);
			}
		}
	}

	protected static void o2m(Object parent, Map<Method, Collection> data, EntityManager em, Map<String, Object> hits) {
		Set<Method> associations = data.keySet();
		for (Method method : associations) {
			Collection<Serializable> list = data.get(method);
			for (Serializable kid : list) {
				Object kid2 = checkHits(hits, kid);
				if (kid2 == null) {
					//System.out.println("Missed O2M Ref: " + getNaturalId(kid) + " for " + getNaturalId(parent));
					kid2 = (Serializable) doImport(parent, kid, em, hits);
				} else {
					//System.out.println("Hit O2M Ref: " + getNaturalId(kid));
				}
				add(method, parent, kid2);
			}
		}
	}

	//---------------------------------------------------------------
	
	protected static Map<Method, Collection> collect(Object entity, Class annotation) {
		Map<Method, Collection> collections = new HashMap<Method, Collection>();
		
		List<Method> associations = ReflectionUtil.findMethodsByAnnotation(
				entity.getClass(), annotation);
		for (Method method : associations) {
			Collection<Serializable> list = (Collection<Serializable>) ReflectionUtil
					.invoke(method, entity, new Object[] {});
			if (list.size() > 0) {
				set(entity, method.getName(), null);
				//System.out.println("Collection: " + getNaturalId(entity) + " Method: " + method.getName() + " Size: " + list.size());
				collections.put(method, list);
			}
		}
		return collections;
	}

	protected static Object find(Object value, EntityManager em, Map<String, Object> hits) {
		Object ref = checkHits(hits, value);
		if (ref == null) {
			ref = JpaUtil.findForNaturalId((Serializable) value, em);
			if (ref != null) {
				addHit(hits, ref);
			}
		}
		return ref;
	}
	
	protected static Object checkHits(Map<String, Object> hits, Object value) {
		String nid = getNaturalId(value);
		Object hit = hits.get(nid);
		if (hit != null) {
			if (log.isDebugEnabled()) {
				log.debug("NID hit: " + nid);
			}
		} else {
			if (log.isDebugEnabled()) {
				log.debug("NID miss: " + nid);
			}
		}
		return hit;
	}

	protected static void addHit(Map<String, Object> hits, Object value) {
		String nid = getNaturalId(value);
		if (log.isDebugEnabled()) {
			log.debug("Adding NID to hits: " + nid);
		}
		hits.put(nid, value);
	}

	protected static String getNaturalId(Object value) {
		return JpaUtil.toString(value);
	}
	
	protected static void set(Object object, String name, Object value) {
//		log.debug("Set: " + value + " on: " + object + " property: " + name);
		name = "s" + name.substring(1);
		Method method = ReflectionUtil.findMethodByName(object.getClass(), name);
		ReflectionUtil.invoke(method, object, new Object[] { value });
	}

	protected static void add(Method getter, Object object, Object value) {
		Method method = findAdder(object.getClass(), getter.getName(), new Class[] { value.getClass() });
		if (method == null) {
			throw new RuntimeException("Adder not found for: " + getter.getName() + " on: " + object.getClass());
		}
		//if (log.isDebugEnabled()) {
		//	log.debug("Add: " + getNaturalId(value) + " on: " + getNaturalId(object) + " with: " + method.getName());
		//}
		ReflectionUtil.invoke(method, object, new Object[] { value });
	}
	
	protected static Method findAdder(Class type, String getter, Class[] args) {
		Method[] methods = type.getMethods();
		for (int i = 0; i < methods.length; i++) {
			Method method = methods[i];
			if (method.getName().startsWith("add")) {
				Class[] params = method.getParameterTypes();
				if (params.length == args.length) {
					boolean found = false;
					for (int j = 0; j < params.length; j++) {
						if (params[j].isAssignableFrom(args[j])
								&& method.getName().contains(
										getter.substring(3, getter.length()-3))) {
							found = true;
						}
					}
					if (found) {
						return method;
					}
				}
			}
		}
		return null;
	}

	protected static EntityManager getEntityManager(Object entity) {
		String pcn = EntityPersistenceContextMap.instance()
				.getPersistenceContextNameFor(entity.getClass());
		return (EntityManager) Component.getInstance(pcn);
	}
}
